AWS利用料金を毎日Slackに通知する仕組みをCDKで作りたくてやってみた
どーも、データアナリティクス事業本部コンサルティングチームのsutoです。
最近仕事が忙しくなると、AWSにて検証で作ったリソースを削除し忘れたことで余計な課金を発生させてしまうことが増えてきました。
自分の個人検証アカウントではAWS Budgetsを使って予算とアラートを設定していましたが、上限近くになってから気づくより毎日通知で気づくほうが良いと思ったので、今回はAWS CDKを使って作ってみました。
※CDKをTypescriptで書く練習をしたかったという思いもあり、CDKスタックはTypescript、中のLambdaはPythonという個人的趣向に沿った組み合わせとなっています。
作るもの
以下の図のとおりです。
毎日AM9時10分(JST)にAWS料金を特定のSlackチャンネルに通知します。
作業環境は以下となります。(Python、AWS CDKの環境はすでにインストール済みの状態です)
% sw_vers ProductName: macOS ProductVersion: 12.6.2 BuildVersion: 21G320 % python --version Python 3.9.13 % cdk --version 2.60.0 (build 2d40d77)
Slackの設定
- LambdaによるSlack通知はIncoming Webhook URLを使いますのでまずはこちらを生成しておきます。
-
Slackアプリでワークスペースにサインインして通知先とするチャンネル名を控えた後、チャンネル一覧から「その他」→ 「App」をクリックします。
- 右上の「App ディレクトリ」をクリックします。
- ブラウザで以下の画面が表示されるので、右上の「ビルド」をクリックします。
- 「Create an app」をクリックします。
- 以下のポップアップが表示されたら、「From scratch」をクリックします。
- App Name に任意の名前を入力します。
- 今回使用するワークスペースを選択します。
- 「Create App」をクリックします。
- Basic Information の中から「Incoming Webhooks」をクリックします。
-
デフォルトでは右上のトグルが「Off」になっているので、クリックして「On」にします。
- 「Add New Webhook to Workspace」をクリックし、通知先とするチャンネル名を入力して「許可する」をクリックします。
-
Webhook URL が払い出されるのでコピーしておきます。
CDKプロジェクト作成
- プロジェクト用フォルダとCDKプロジェクトを作成します。
% mkdir cdk-billing-alarm && cd cdk-billing-alarm % cdk init --language=typescript % cdk ls CdkBillingAlarmStack
Lambda Function作成
- 「lambda」というフォルダを作成し、その配下のファイル「app.py」にコードを書いて保存します。
% mkdir lambda
# lambda/app.py # encoding: utf-8 import json import datetime import requests import boto3 import os import logging TODAY = datetime.datetime.utcnow() BEGINING_OF_THE_MONTH = TODAY - datetime.timedelta(days=TODAY.day - 1) START_DATE = BEGINING_OF_THE_MONTH.strftime('%Y/%m/%d').replace('/', '-') END_DATE = TODAY.strftime('%Y/%m/%d').replace('/', '-') SLACK_POST_URL = os.environ['SLACK_POST_URL'] SLACK_CHANNEL = os.environ['SLACK_CHANNEL'] logger = logging.getLogger() logger.setLevel(logging.INFO) client = boto3.client('ce') sts = boto3.client('sts') id_info = sts.get_caller_identity() def get_total_cost(): response = client.get_cost_and_usage( TimePeriod={ 'Start': START_DATE, 'End': END_DATE }, Granularity='MONTHLY', Metrics=[ 'UnblendedCost', ], ) total_cost = response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"] return total_cost def handler(event, context): text = "ID:{} の {}までのAWS合計料金 : ${}".format(id_info['Account'], END_DATE, get_total_cost()) content = {"text": text} slack_message = { 'channel': SLACK_CHANNEL, "attachments": [content], } try: requests.post(SLACK_POST_URL, data=json.dumps(slack_message)) except requests.exceptions.RequestException as e: logger.error("Request failed: %s", e)
Lambda Layer作成
- コード内のモジュール「requests」は外部モジュールなので、そのためのLambda Layerを作ります。
-
以下のようにLambda Layer 用のディレクトリを作成し、その配下にpythonフォルダを作ってインポートします。
% mkdir lambda_layer && cd lambda_layer % mkdir python % pip install -t python requests
CDKスタック作成
- cdk-billing-alarm-stack.tsを編集します。
- コード内の「SLACK_POST_URLの値」と「SLACK_CHANNELの値」を控えておいた情報に書き換えて保存します。
// lib/cdk-billing-alarm-stack.ts import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as events from "aws-cdk-lib/aws-events"; import * as targets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; export class CdkBillingAlarmStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // lambda-layer const layer = new lambda.LayerVersion(this, 'MyLayer', { code: lambda.Code.fromAsset("lambda_layer"), compatibleRuntimes: [lambda.Runtime.PYTHON_3_9], }); // lambda const sampleLambda = new lambda.Function(this, 'NptifyPriceHandler', { runtime: lambda.Runtime.PYTHON_3_9, // execution environment code: lambda.Code.fromAsset('lambda'), // code loaded from "lambda" directory handler: 'app.handler', // file is "hello", function is "handler" environment: { TZ: 'Asia/Tokyo', SLACK_POST_URL: '生成したSlackのWebhook URL', SLACK_CHANNEL: '通知先のチャンネル名', }, layers: [layer], initialPolicy: [new iam.PolicyStatement({ actions: ['ce:GetCostAndUsage'], resources: ['*'], })], }); // EventBridge new events.Rule(this, "sampleRule", { // JST で毎日 AM9:10 に定期実行 // 参考 https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions schedule: events.Schedule.cron({minute: "10", hour: "0"}), targets: [new targets.LambdaFunction(sampleLambda, {retryAttempts: 3})], }); } }
- 最終的にフォルダ構成はこのようなかたちとなります。
デプロイ
- 初めてCDKデプロイをする方のみbootstrap を実行します。
% cdk bootstrap
- 作成したスタックをデプロイします。
% cdk deploy CdkBillingAlarmStack
- デプロイ完了後、Lambdaの動作を確認してみると、以下のようにSlackチャンネルに通知することができました。